package network

import (
	"encoding/json"
	"github.com/sirupsen/logrus"
	"mydocker/common"
	"net"
	"os"
	"path"
	"strings"
)

type IPAM struct {
	SubnetAllocatorPath string             // 分配文件存放位置
	Subnets             *map[string]string // 网段和位图算法的数组 map, key 是网段，value 是分配的位图数组
}

// 初始化一个 IPAM 的对象，默认使用 /var/lib/my-docker/network/ipam/subnet.json 作为分配信息存储位置
var ipAllocator = &IPAM{
	SubnetAllocatorPath: common.DefaultAllocatorPath,
}

// load 加载网段地址分配信息
func (ipam *IPAM) load() error {
	// 检查存储文件状态，如果不存在，则说明之前没有分配，则不需要加载
	if _, err := os.Stat(ipam.SubnetAllocatorPath); err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		return nil
	}
	// 读取文件，加载配置信息
	subnetConfigFile, err := os.Open(ipam.SubnetAllocatorPath)
	if err != nil {
		return err
	}
	defer subnetConfigFile.Close()

	subnetJson := make([]byte, 2000)
	n, err := subnetConfigFile.Read(subnetJson)
	if err != nil {
		return err
	}

	return json.Unmarshal(subnetJson[:n], ipam.Subnets)
}

// dump 存储网段地址分配信息
func (ipam *IPAM) dump() error {
	ipamConfigFileDir, _ := path.Split(ipam.SubnetAllocatorPath) // 分割文件的名称和路径，将文件的路径分割成 dir+file
	if _, err := os.Stat(ipamConfigFileDir); err != nil && os.IsNotExist(err) {
		if err = os.MkdirAll(ipamConfigFileDir, common.Perm0644); err != nil {
			return err
		}
	}
	// 打开存储文件 O_TRUNC 表示如果存在则消空，os.O_CREATE 表示如果不存在则创建
	subnetConfigFile, err := os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, common.Perm0644)
	if err != nil {
		return err
	}
	defer subnetConfigFile.Close()

	ipamConfigJson, err := json.Marshal(ipam.Subnets)
	if err != nil {
		return err
	}
	_, err = subnetConfigFile.Write(ipamConfigJson)
	return err
}

// Allocate 在网段中分配一个可用的 IP 地址
func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) {
	// 存放网段中地址分配信息的数组
	ipam.Subnets = &map[string]string{}
	// 从文件中加载已经分配的网段信息
	err = ipam.load()
	if err != nil {
		logrus.Errorf("load subnet allocation info error: %v", err)
		return nil, err
	}
	// net.IPNet.Mask.Size 函数会返回网段的子网掩码的总长度和网段前面的固定位的长度
	// 比如："127.0.0.0/8" 网段的子网掩码是 "255.0.0.0"。
	// 那么 subnet.Mask.Size() 的返回值就是前面255所对应的位数和总位数，即 8 和 32
	_, subnet, err = net.ParseCIDR(subnet.String())
	if err != nil {
		return nil, err
	}
	one, size := subnet.Mask.Size() // 返回子网掩码中连续的 1 的位数，这对应于子网掩码的“前缀长度”

	// 检查子网是否已经存在于分配信息中，如果不存在则初始化为全 0 字符串
	if _, exist := (*ipam.Subnets)[subnet.String()]; !exist { // 访问 subnet.String() 作为键的值
		// 用 “0” 填满这个网段的配置，uint8(size - one) 表示这个网段中有多少个可用地址
		// size - one 是子网掩码后面的网络位数，2^(size - one)表示这个网段中有多少个可用地址
		// 而 2^(size - one) 等价于 1 << uint8(size - one)
		// 左移一位就是扩大两倍
		// 比如：如果 subnet.String() 是 192.168.0.0/16，那么这里 one = 16，size = 32， 下面式子的值就是 2^(32-16) 位的 0 组成的字符串
		(*ipam.Subnets)[subnet.String()] = strings.Repeat("0", 1<<uint8(size-one)) // 创建一个由“0”组成的字符串，长度为 2^(size - one)
	}

	// 遍历网段的位图数组。如果是上面的例子的话，这里 i 的值就是 0 ~ 2^(32-16)-1
	for i := range (*ipam.Subnets)[subnet.String()] { // 遍历键为 subnet.String() 的值（字符串）
		// 找到数组中为 “0” 的项和数组序号，即可以分配的 IP
		if (*ipam.Subnets)[subnet.String()][i] == '0' {
			// 设置这个为 “0” 的序号值为 “1”，即标记这个 IP 已经分配过了
			ipAlloc := []byte((*ipam.Subnets)[subnet.String()])
			ipAlloc[i] = '1'
			(*ipam.Subnets)[subnet.String()] = string(ipAlloc)
			// 这里的 subnet.IP 只是初始 IP，比如对于网段 192.168.0.0/16，这里就是 192.168.0.0
			ip = subnet.IP
			/*
				还需要通过网段的 IP 与上面的偏移相加计算出分配的 IP 地址，由于 IP 地址是 unit 的一个数组，
				需要通过数组中的每一项加所需要的值，比如网段是 172.16.0.0/12，数组序号就是 65555（本例中数组序号范围是 0 ~ 2^(32-16)），
				那么在 [172,16,0,0] 上依次加 [uint8(65555 >> 24)、uint8(65555 >> 16)、
				uint8(65555 >> 8)、uint8(65555 >> 0)]，即[0, 1, 0, 19]，那么获得的 IP 就是 172.17.0.19
			*/
			for t := uint(4); t > 0; t -= 1 {
				[]byte(ip)[4-t] += uint8(i >> ((t - 1) * 8))
			}
			// 由于此处 IP 是从 1 开始分配的（0被网关占了），所以最后再加 1，所以上面例子最终分配的 IP 为 172.17.0.18
			ip[3] += 1
			break // 找到一个可用的就退出了
		}
	}
	// 最后调用 dump 将分配结果保存到文件中
	err = ipam.dump()
	if err != nil {
		logrus.Errorf("allocate ip, dump ipam info, err: %v\n", err)
		return nil, err
	}
	return ip, nil
}

// Release 从指定的 subnet 网段中释放指定的 IP 地址
func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error {
	ipam.Subnets = &map[string]string{}
	_, ipNet, err := net.ParseCIDR(subnet.String())
	if err != nil {
		return err
	}

	err = ipam.load()
	if err != nil {
		logrus.Errorf("dump allocation info, err: %v\n", err)
		return err
	}
	// 和分配一样的算法，反过来根据 IP 找到位图数组中的对应索引位置
	c := 0
	releaseIP := ipaddr.To4() // 将 IP 地址转换为 IPv4 地址
	releaseIP[3] -= 1
	for t := uint(4); t > 0; t -= 1 {
		c += int(releaseIP[t-1]-ipNet.IP[t-1]) << ((4 - t) * 8)
	}
	// 然后将对应位 置0
	ipAlloc := []byte((*ipam.Subnets)[ipNet.String()])
	ipAlloc[c] = '0'
	(*ipam.Subnets)[ipNet.String()] = string(ipAlloc)

	// 最后调用 dump 将分配结果保存到文件中
	err = ipam.dump()
	if err != nil {
		logrus.Errorf("release ip, dump info, err: %v", err)
		return err
	}
	return nil
}
